<script setup>
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { computed, defineExpose, onMounted, onUnmounted, ref, watch } from 'vue'
import 'xterm/css/xterm.css'
import { EventsEmit, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js'
import { get, isEmpty, set } from 'lodash'
import { CloseCli, StartCli } from 'wailsjs/go/services/cliService.js'
import usePreferencesStore from 'stores/preferences.js'
import { i18nGlobal } from '@/utils/i18n.js'

const props = defineProps({
  name: String,
  activated: Boolean,
})

const prefStore = usePreferencesStore()
const termRef = ref(null)
/**
 *
 * @type {xterm.Terminal|null}
 */
let termInst = null
/**
 *
 * @type {xterm-addon-fit.FitAddon|null}
 */
let fitAddonInst = null

/**
 *
 * @return {{fitAddon: xterm-addon-fit.FitAddon, term: Terminal}}
 */
const newTerm = () => {
  const { fontSize = 14, fontFamily = 'Courier New' } = prefStore.cliFont
  const term = new Terminal({
    allowProposedApi: true,
    fontFamily,
    fontSize,
    cursorStyle: prefStore.cli.cursorStyle || 'block',
    cursorBlink: true,
    disableStdin: false,
    screenReaderMode: true,
    // LogLevel: 'debug',
    theme: {
      // foreground: '#ECECEC',
      background: '#000000',
      // cursor: 'help',
      // lineHeight: 20,
    },
  })
  const fitAddon = new FitAddon()
  term.open(termRef.value)
  term.loadAddon(fitAddon)

  term.onData(onTermData)
  term.attachCustomKeyEventHandler(onTermKey)
  return { term, fitAddon }
}

onMounted(() => {
  const { term, fitAddon } = newTerm()
  termInst = term
  fitAddonInst = fitAddon
  window.addEventListener('resize', resizeTerm)

  term.writeln('\r\n' + i18nGlobal.t('interface.cli_welcome'))
  // term.write('\x1b[4h') // insert mode
  CloseCli(props.name)
  StartCli(props.name, 0)

  EventsOn(`cmd:output:${props.name}`, receiveTermOutput)
  fitAddon.fit()
  term.focus()
})

onUnmounted(() => {
  window.removeEventListener('resize', resizeTerm)
  EventsOff(`cmd:output:${props.name}`)
  termInst.dispose()
  termInst = null
  console.warn('destroy term')
})

const resizeTerm = () => {
  if (fitAddonInst != null) {
    fitAddonInst.fit()
  }
}

defineExpose({
  resizeTerm,
})

watch(
  () => prefStore.cliFont,
  ({ fontSize = 14, fontFamily = 'Courier New' }) => {
    if (termInst != null) {
      termInst.options.fontSize = fontSize
      termInst.options.fontFamily = fontFamily
    }
    resizeTerm()
  },
)

watch(
  () => prefStore.cli.cursorStyle,
  (style) => {
    if (termInst != null) {
      termInst.options.cursorStyle = style || 'block'
    }
    resizeTerm()
  },
)

const prefixContent = computed(() => {
  return '\x1b[33m' + promptPrefix.value + '\x1b[0m'
})

const prefixLen = computed(() => {
  let len = 0
  for (let i = 0; i < promptPrefix.value.length; i++) {
    const char = promptPrefix.value.charCodeAt(i)
    if (char >= 0x0000 && char <= 0x00ff) {
      // single byte ASCII char
      len += 1
    } else {
      // multibyte Unicode char
      len += 2
    }
  }
  return len
})

let promptPrefix = ref('')
let inputCursor = 0
const inputHistory = []
let historyIndex = 0
let waitForOutput = false
const onTermData = (data) => {
  if (termInst == null) {
    return
  }

  if (data) {
    const cc = data.charCodeAt(0)
    switch (cc) {
      case 127: // backspace
        deleteInput(true)
        return

      case 13: // enter
        // try to process local command first
        switch (getCurrentInput()) {
          case 'clear':
          case 'clr':
            termInst.clear()
            replaceTermInput()
            newInputLine()
            return

          default: // send command to server
            flushTermInput()
            return
        }

      case 27:
        switch (data.substring(1)) {
          case '[A': // arrow up
            changeHistory(true)
            return
          case '[B': // arrow down
            changeHistory(false)
            return
          case '[C': // arrow right ->
            moveInputCursor(1)
            return
          case '[D': // arrow left <-
            moveInputCursor(-1)
            return
          case '[3~': // del
            deleteInput(false)
            return
        }

      case 9: // tab
        return
    }
  }

  updateInput(data)
  // term.write(data)
}

/**
 *
 * @param e
 * @return {boolean}
 */
const onTermKey = (e) => {
  if (e.type === 'keydown') {
    if (e.ctrlKey) {
      switch (e.key) {
        case 'a': // move to head of line
          moveInputCursorTo(0)
          return false

        case 'e': // move to tail of line
          moveInputCursorTo(Number.MAX_SAFE_INTEGER)
          return false

        case 'f': // move forward
          moveInputCursor(1)
          return false

        case 'b': // move backward
          moveInputCursor(-1)
          return false

        case 'd': // delete char
          deleteInput(false)
          return false

        case 'h': // back delete
          deleteInput(true)
          return false

        case 'u': // delete all text before cursor
          deleteInput2(false)
          return false

        case 'k': // delete all text after cursor
          deleteInput2(true)
          return false

        case 'w': // delete word before cursor
          deleteWord(false)
          return false

        case 'p': // previous history
          changeHistory(true)
          return false

        case 'n': // next history
          changeHistory(false)
          return false

        case 'l': // clear screen
          termInst.clear()
          replaceTermInput()
          newInputLine()
          return false
      }
      // block all ctrl key combinations input
      return false
    }
  }
  return true
}

/**
 * move input cursor by step
 * @param {number} step above 0 indicate move right; 0 indicate move to last
 */
const moveInputCursor = (step) => {
  if (termInst == null) {
    return
  }

  let updateCursor = false
  if (step > 0) {
    // move right
    const currentLine = getCurrentInput()
    if (inputCursor + step <= currentLine.length) {
      inputCursor += step
      updateCursor = true
    }
  } else if (step < 0) {
    // move left
    if (inputCursor + step >= 0) {
      inputCursor += step
      updateCursor = true
    }
  }

  if (updateCursor) {
    moveInputCursorTo(inputCursor)
  }
}

/**
 * move cursor to the end of current line
 */
const moveInputCursorToEnd = () => {
  moveInputCursorTo(Number.MAX_SAFE_INTEGER)
}

/**
 * move cursor to pos
 * @param {number} pos
 */
const moveInputCursorTo = (pos) => {
  const currentLine = getCurrentInput()
  inputCursor = Math.min(Math.max(0, pos), currentLine.length)
  termInst.write(`\x1B[${prefixLen.value + inputCursor + 1}G`)
}

/**
 * update current input cache and refresh term
 * @param {string} data
 */
const updateInput = (data) => {
  if (data == null || data.length <= 0) {
    return
  }

  if (termInst == null) {
    return
  }

  let currentLine = getCurrentInput()
  if (inputCursor < currentLine.length) {
    // insert
    currentLine = currentLine.substring(0, inputCursor) + data + currentLine.substring(inputCursor)
    replaceTermInput(currentLine)
    moveInputCursor(data.length)
  } else {
    // append
    currentLine += data
    termInst.write(data)
    inputCursor += data.length
  }
  updateCurrentInput(currentLine)
}

/**
 *
 * @param {boolean} back backspace or not
 */
const deleteInput = (back = false) => {
  if (termInst == null) {
    return
  }

  let currentLine = getCurrentInput()
  if (inputCursor < currentLine.length) {
    // delete middle part
    if (back) {
      currentLine = currentLine.substring(0, inputCursor - 1) + currentLine.substring(inputCursor)
      inputCursor -= 1
    } else {
      currentLine = currentLine.substring(0, inputCursor) + currentLine.substring(inputCursor + 1)
    }
  } else {
    if (back) {
      // delete last one
      currentLine = currentLine.slice(0, -1)
      inputCursor -= 1
    }
  }

  replaceTermInput(currentLine)
  updateCurrentInput(currentLine)
  moveInputCursorTo(inputCursor)
}

/**
 * delete to the end
 * @param back
 */
const deleteInput2 = (back = false) => {
  if (termInst == null) {
    return
  }

  let currentLine = getCurrentInput()
  if (back) {
    // delete until tail
    currentLine = currentLine.substring(0, inputCursor - 1)
    inputCursor = currentLine.length
  } else {
    // delete until head
    currentLine = currentLine.substring(inputCursor)
    inputCursor = 0
  }

  replaceTermInput(currentLine)
  updateCurrentInput(currentLine)
  moveInputCursorTo(inputCursor)
}

/**
 * delete one word
 * @param back
 */
const deleteWord = (back = false) => {
  if (termInst == null) {
    return
  }

  let currentLine = getCurrentInput()
  if (back) {
    const prefix = currentLine.substring(0, inputCursor)
    let firstNonChar = false
    let cursor = inputCursor
    while (cursor < currentLine.length) {
      const isChar =
        (currentLine[cursor] >= 'a' && currentLine[cursor] <= 'z') ||
        (currentLine[cursor] >= 'A' && currentLine[cursor] <= 'Z') ||
        (currentLine[cursor] >= '0' && currentLine[cursor] <= '9')
      if (!firstNonChar || isChar) {
        if (!isChar) {
          firstNonChar = true
        }
        cursor++
      } else {
        break
      }
    }
    currentLine = prefix + currentLine.substring(cursor)
  } else {
    const suffix = currentLine.substring(inputCursor)
    let firstNonChar = false
    while (inputCursor >= 0) {
      const isChar =
        (currentLine[inputCursor] >= 'a' && currentLine[inputCursor] <= 'z') ||
        (currentLine[inputCursor] >= 'A' && currentLine[inputCursor] <= 'Z') ||
        (currentLine[inputCursor] >= '0' && currentLine[inputCursor] <= '9')
      if (!firstNonChar || isChar) {
        if (!isChar) {
          firstNonChar = true
        }
        inputCursor--
      } else {
        break
      }
    }
    currentLine = currentLine.substring(0, inputCursor) + suffix
  }

  replaceTermInput(currentLine)
  updateCurrentInput(currentLine)
  moveInputCursorTo(inputCursor)
}

const getCurrentInput = () => {
  return get(inputHistory, historyIndex, '')
}

const updateCurrentInput = (input) => {
  set(inputHistory, historyIndex, input || '')
}

const newInputLine = () => {
  if (historyIndex >= 0 && historyIndex < inputHistory.length - 1) {
    // edit prev history, move to last
    const pop = inputHistory.splice(historyIndex, 1)
    inputHistory[inputHistory.length - 1] = pop[0]
  }
  if (get(inputHistory, inputHistory.length - 1, '')) {
    historyIndex = inputHistory.length
    updateCurrentInput('')
  }
}

/**
 * get prev or next history record
 * @param prev
 * @return {*|null}
 */
const changeHistory = (prev) => {
  let currentLine = null
  if (prev) {
    if (historyIndex > 0) {
      historyIndex -= 1
      currentLine = inputHistory[historyIndex]
    }
  } else {
    if (historyIndex < inputHistory.length - 1) {
      historyIndex += 1
      currentLine = inputHistory[historyIndex]
    }
  }

  if (currentLine != null) {
    if (termInst == null) {
      return
    }

    replaceTermInput(currentLine)
    moveInputCursorToEnd()
  }

  return null
}

/**
 * flush terminal input and send current prompt to server
 * @param {boolean} flushCmd
 */
const flushTermInput = (flushCmd = false) => {
  const currentLine = getCurrentInput()
  EventsEmit(`cmd:input:${props.name}`, currentLine)
  inputCursor = 0
  // historyIndex = inputHistory.length
  waitForOutput = true
}

/**
 * clear current input line and replace with new content
 * @param {string|null} [content]
 */
const replaceTermInput = (content = '') => {
  if (termInst == null) {
    return
  }

  // erase current line and write new content
  termInst.write('\r\x1B[K' + prefixContent.value + (content || ''))
}

/**
 * process receive output content
 * @param {{content: string[], prompt: string}} data
 */
const receiveTermOutput = (data) => {
  if (termInst == null) {
    return
  }

  const { content = [], prompt } = data || {}
  if (!isEmpty(content)) {
    for (const line of content) {
      termInst.write('\r\n' + line)
    }
  }
  if (!isEmpty(prompt)) {
    promptPrefix.value = prompt
    termInst.write('\r\n' + prefixContent.value)
    waitForOutput = false
    inputCursor = 0
    newInputLine()
  }
}
</script>

<template>
  <div ref="termRef" class="xterm" />
</template>

<style lang="scss" scoped>
.xterm {
  width: 100%;
  min-height: 100%;
  overflow: hidden;
  background-color: #000000;
}
</style>

<style lang="scss">
.xterm-screen {
  padding: 0 5px !important;
}

.xterm-viewport::-webkit-scrollbar {
  background-color: #000000;
  width: 5px;
}

.xterm-viewport::-webkit-scrollbar-thumb {
  background: #000000;
}

.xterm-decoration-overview-ruler {
  right: 1px;
  pointer-events: none;
}
</style>
